
/*
 * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net)
 */

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

#include "ngx_http_xtoken_misc.h"

#define XTOKEN_MAX_BUCKETS 86400 // 1 day of history at 1s clean frequency
#define XTOKEN_SHM_SIZE    (sizeof(ngx_http_xtoken_shctx_t))

typedef struct {
	ngx_flag_t enable_server;
	ngx_flag_t enable_filter;
	ngx_flag_t enable_clear_cookies;

	ngx_str_t  cookie_name_hash;
	ngx_str_t  cookie_name_expires;
	ngx_str_t  cookie_name_uid;
	ngx_str_t  cookie_name_location;
	ngx_str_t  cookie_domain;
	ngx_str_t  cookie_path;

	ngx_str_t  url_server;           /* URL where to find the server */
	ngx_str_t  url_cookie;           /* URL where to show that cookies have to be enabled */
	ngx_str_t  url_overload;         /* URL where to show that there is too much users */
	ngx_str_t  url_error;            /* URL where to show that there where an error */

	ngx_str_t  key;                  /* symmetric key to 'encrypt' md5 hash within HMAC */
	time_t     expires_server;       /* time during which a new UID is valid */
	time_t     expires;              /* session duration */
	time_t     clean_freq;           /* clean frequency, to remove timeouted session */
	ngx_uint_t max_users;            /* max number of users allowed on the system */

	ngx_shm_zone_t *shm_zone;        /* SHM zone */
	ngx_str_t       shm_name;        /* the name of the SHM zone */
} ngx_http_xtoken_loc_conf_t;

typedef struct {
	ngx_uint_t  current;                     /* current number of users on the system */
	time_t      next_clean;                  /* time when a cleaner cleand the buckets */
	ngx_uint_t  buckets[XTOKEN_MAX_BUCKETS]; /* the buckets containing the number of users for each clean time interval */
} ngx_http_xtoken_shctx_t;

typedef struct {
	ngx_http_xtoken_shctx_t *sh;
	ngx_slab_pool_t         *shpool;
	ngx_uint_t               nbuckets; /* Number of buckets */
} ngx_http_xtoken_ctx_t;

static char      *ngx_http_xtoken_set_server           (ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char      *ngx_http_xtoken_set_clear_cookies    (ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void      *ngx_http_xtoken_create_loc_conf      (ngx_conf_t *cf);
static char      *ngx_http_xtoken_merge_loc_conf       (ngx_conf_t *cf, void *parent, void *child);
static ngx_int_t  ngx_http_xtoken_init                 (ngx_conf_t *cf);
static ngx_int_t  ngx_http_xtoken_init_worker          (ngx_cycle_t *cycle);

static ngx_int_t  ngx_http_xtoken_init_zone            (ngx_shm_zone_t *shm_zone, void *data);

static ngx_int_t ngx_http_xtoken_filter_handler        (ngx_http_request_t *r);
static ngx_int_t ngx_http_xtoken_server_handler        (ngx_http_request_t *r);
static ngx_int_t ngx_http_xtoken_clear_cookies_handler (ngx_http_request_t *r);
static ngx_int_t ngx_http_xtoken_clean_buckets         (ngx_http_xtoken_ctx_t *ctx, ngx_http_xtoken_loc_conf_t *conf, ngx_http_xtoken_shctx_t *shm, ngx_http_request_t *r);

static ngx_int_t ngx_http_xtoken_redirect_to_server    (ngx_http_request_t *r, ngx_http_xtoken_loc_conf_t *conf);
static ngx_int_t ngx_http_xtoken_make_hash             (ngx_http_request_t *r, ngx_http_xtoken_loc_conf_t *conf, ngx_str_t *expires, ngx_str_t *uid, ngx_str_t *hash);

static ngx_command_t  ngx_http_xtoken_commands[] = {

	{ ngx_string("xtoken_server"),
		NGX_HTTP_LOC_CONF|NGX_CONF_ANY,
		ngx_http_xtoken_set_server,
		NGX_HTTP_LOC_CONF_OFFSET,
		0,
		NULL },

	{ ngx_string("xtoken_filter"),
		NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
		ngx_conf_set_flag_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, enable_filter),
		NULL },

	{ ngx_string("xtoken_clear_cookies"),
		NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
		ngx_http_xtoken_set_clear_cookies,
		NGX_HTTP_LOC_CONF_OFFSET,
		0,
		NULL },

	{ ngx_string("xtoken_cookie_name_expires"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_name_expires),
		NULL },

	{ ngx_string("xtoken_cookie_name_hash"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_name_hash),
		NULL },

	{ ngx_string("xtoken_cookie_name_uid"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_name_uid),
		NULL },

	{ ngx_string("xtoken_cookie_name_location"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_name_location),
		NULL },

	{ ngx_string("xtoken_cookie_domain"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_domain),
		NULL },

	{ ngx_string("xtoken_cookie_path"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, cookie_path),
		NULL },

	{ ngx_string("xtoken_url_server"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, url_server),
		NULL },

	{ ngx_string("xtoken_url_overload"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, url_overload),
		NULL },

	{ ngx_string("xtoken_url_cookie"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, url_cookie),
		NULL },

	{ ngx_string("xtoken_url_error"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, url_error),
		NULL },

	{ ngx_string("xtoken_key"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_str_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, key),
		NULL },

	{ ngx_string("xtoken_expires_server"),
		NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
		ngx_conf_set_sec_slot,
		NGX_HTTP_LOC_CONF_OFFSET,
		offsetof(ngx_http_xtoken_loc_conf_t, expires_server),
		NULL },

	ngx_null_command
};


static ngx_http_module_t  ngx_http_xtoken_module_ctx = {
	NULL,                            /* preconfiguration */
	ngx_http_xtoken_init,            /* postconfiguration */

	NULL,                            /* create main configuration */
	NULL,                            /* init main configuration */

	NULL,                            /* create server configuration */
	NULL,                            /* merge server configuration */

	ngx_http_xtoken_create_loc_conf, /* create location configuration */
	ngx_http_xtoken_merge_loc_conf   /* merge location configuration */
};


ngx_module_t  ngx_http_xtoken_module = {
	NGX_MODULE_V1,
	&ngx_http_xtoken_module_ctx, /* module context */
	ngx_http_xtoken_commands,   /* module directives */
	NGX_HTTP_MODULE,               /* module type */
	NULL,                          /* init master */
	NULL,                          /* init module */
	NULL,                          /* init process */
	ngx_http_xtoken_init_worker,   /* init thread */
	NULL,                          /* exit thread */
	NULL,                          /* exit process */
	NULL,                          /* exit master */
	NGX_MODULE_V1_PADDING
};

static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
static uint32_t  start_value;
static uint32_t  sequencer = 0x03030302;

static ngx_int_t ngx_http_xtoken_make_hash(ngx_http_request_t *r, ngx_http_xtoken_loc_conf_t *conf, ngx_str_t *expires, ngx_str_t *uid, ngx_str_t *hash)
{
	ngx_str_t message;

	message.data = ngx_pcalloc(r->pool, expires->len + uid->len + 1);
	if (message.data == NULL) {
		return NGX_ERROR;
	}
	message.len = expires->len + uid->len;

	ngx_snprintf(message.data, expires->len + uid->len, "%V%V", expires, uid);

	if (ngx_http_xtoken_misc_hmac_md5(r, &conf->key, &message, hash) != NGX_OK) {
		return NGX_ERROR;
	}

	return NGX_OK;
}

static ngx_int_t ngx_http_xtoken_redirect_to_server(ngx_http_request_t *r, ngx_http_xtoken_loc_conf_t *conf) {
	ngx_str_t uid, url, expires, hash;

	if (ngx_http_xtoken_misc_forge_url(r, &url, 1) == NGX_ERROR) {
		return NGX_ERROR;
	}

	if (conf->cookie_name_location.len > 0) {
		if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_location, &url, &conf->cookie_domain, &conf->cookie_path, conf->expires_server + 10) != NGX_OK) {
			return NGX_ERROR;
		}
	}

	if (ngx_http_xtoken_misc_generate_uid(r, start_value, &sequencer, &uid) != NGX_OK) {
		return NGX_ERROR;
	}

	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_uid, &uid, &conf->cookie_domain, &conf->cookie_path, conf->expires + 10) != NGX_OK) {
		return NGX_ERROR;
	}

	expires.len = sizeof("1275396350") - 1;
	expires.data = ngx_pnalloc(r->pool, sizeof("1275396350"));
	if  (expires.data == NULL) {
		return NGX_ERROR;
	}
	ngx_snprintf(expires.data, expires.len, "%T", ngx_time());

	if (ngx_http_xtoken_make_hash(r, conf, &expires, &uid, &hash) != NGX_OK) {
		return NGX_ERROR;
	}

	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_expires, &expires, &conf->cookie_domain, &conf->cookie_path, conf->expires_server + 10) != NGX_OK) {
		return NGX_ERROR;
	}
	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_hash, &hash, &conf->cookie_domain, &conf->cookie_path, conf->expires_server + 10)) {
		return NGX_ERROR;
	}

	return ngx_http_xtoken_misc_redirect(r, &conf->url_server);
}

static ngx_int_t ngx_http_xtoken_filter_handler(ngx_http_request_t *r)
{
	ngx_http_xtoken_loc_conf_t  *conf;
	ngx_table_elt_t  **h;
	ngx_str_t cookie_expires, cookie_hash, cookie_uid, hash;
	time_t expires;

	h = r->headers_in.cookies.elts;

	conf = ngx_http_get_module_loc_conf(r, ngx_http_xtoken_module);

	if (!conf->enable_filter) return NGX_DECLINED;

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_expires, &cookie_expires) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] client did not send cookie \"%V\"", &conf->cookie_name_expires);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}
	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_hash, &cookie_hash) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] client did not send cookie \"%V\"", &conf->cookie_name_hash);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}
	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_uid, &cookie_uid) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] client did not send cookie \"%V\"", &conf->cookie_name_uid);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	if (cookie_uid.len != NGX_XTOKEN_HASH_LENGTH) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] cookie \"%V\" has a wrong size (%d)", &conf->cookie_name_uid, NGX_XTOKEN_HASH_LENGTH);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	expires = ngx_atotm(cookie_expires.data, cookie_expires.len);
	if (expires == NGX_ERROR) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] cookie \"%V\" has a wrong format (/^\\d+$)", &conf->cookie_name_expires);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	if (expires < ngx_time()) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] cookie \"%V\" has expired", &conf->cookie_name_expires);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	if (cookie_hash.len != NGX_XTOKEN_HASH_LENGTH) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] cookie \"%V\" has a wrong size (%d)", &conf->cookie_name_hash, NGX_XTOKEN_HASH_LENGTH);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	if (ngx_http_xtoken_make_hash(r, conf, &cookie_expires, &cookie_uid, &hash) != NGX_OK) {
		return NGX_ERROR;
	}

	if (ngx_strncmp(hash.data, cookie_hash.data, hash.len) != 0) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/filter] cookie \"%V\" is a wrong hash", &conf->cookie_name_hash);
		return ngx_http_xtoken_redirect_to_server(r, conf);
	}

	/* keep going - access is allowed */
	return NGX_DECLINED;
}

static ngx_int_t ngx_http_xtoken_clean_buckets(ngx_http_xtoken_ctx_t *ctx, ngx_http_xtoken_loc_conf_t *conf, ngx_http_xtoken_shctx_t *shm, ngx_http_request_t *r)
{
	time_t now = ngx_time();
	time_t i;
	ngx_uint_t n;

	for (i=shm->next_clean, n=0; i<=now && n<ctx->nbuckets; i += conf->clean_freq, n++) {
		ngx_uint_t bucket;
		ngx_uint_t index;

		index = (i / conf->clean_freq + 1) % ctx->nbuckets;

		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/clean_buckets] clean bucket %d", index);

		ngx_shmtx_lock(&ctx->shpool->mutex);
		shm->next_clean = now + conf->clean_freq;
		bucket = shm->buckets[index];
		if (bucket) {
			shm->buckets[index] = 0;
			if (shm->current > bucket) {
				shm->current -= bucket;
			} else {
				shm->current = 0;
			}
		}
		ngx_shmtx_unlock(&ctx->shpool->mutex);
	}
	return NGX_OK;
}

static ngx_int_t ngx_http_xtoken_server_handler(ngx_http_request_t *r)
{
	ngx_http_xtoken_loc_conf_t  *conf;
	ngx_http_xtoken_ctx_t *ctx;
	ngx_http_xtoken_shctx_t *shm;
	ngx_str_t cookie_uid, cookie_hash, cookie_expires, cookie_location, hash, new_expires, url;
	time_t expires;
	ngx_uint_t index;

	if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_POST))) {
		return NGX_HTTP_NOT_ALLOWED;
	}

	conf = ngx_http_get_module_loc_conf(r, ngx_http_xtoken_module);
	if (!conf->enable_server) return NGX_DECLINED;

	if (conf->shm_zone == NULL) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/server] shm not found");
		return NGX_DECLINED;
	}

	ctx = conf->shm_zone->data;
	shm = ctx->sh;

	ngx_http_xtoken_clean_buckets(ctx, conf, shm, r);

	if (shm->current >= conf->max_users) {
		return ngx_http_xtoken_misc_redirect(r, &conf->url_overload);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_location, &cookie_location) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/server] \"%V\" cookie have not been found", &conf->cookie_name_location);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_cookie);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_uid, &cookie_uid) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/server] client did not send cookie \"%V\". Redirect to \"%V\"", &conf->cookie_name_uid, &conf->url_cookie);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_cookie);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_hash, &cookie_hash) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/server] \"%V\" cookie have not been found", &conf->cookie_name_hash);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_cookie);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_expires, &cookie_expires) == NGX_DECLINED) {
		ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "[xtoken/server] \"%V\" cookie have not been found", &conf->cookie_name_expires);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_cookie);
	}

	if (cookie_uid.len != NGX_XTOKEN_HASH_LENGTH) {
		ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[xtoken/server] cookie \"%V\" is not long enough (%d)", &conf->cookie_name_uid, NGX_XTOKEN_HASH_LENGTH);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_error);
	}

	expires = ngx_atotm(cookie_expires.data, cookie_expires.len);
	if (expires == NGX_ERROR) {
		ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[xtoken/server] cookie \"%V\" has a wrong format (/^\\d+$)", &conf->cookie_name_expires);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_error);
	}

	if (expires > ngx_time() + conf->expires_server) {
		ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[xtoken/server] cookie \"%V\" has expired", &conf->cookie_name_expires);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_error);
	}

	if (ngx_http_xtoken_make_hash(r, conf, &cookie_expires, &cookie_uid, &hash) != NGX_OK) {
		return NGX_ERROR;
	}

	if (ngx_strncmp(hash.data, cookie_hash.data, hash.len) != 0) {
		ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[xtoken/server] cookie \"%V\" is a wrong hash", &conf->cookie_name_hash);
		return ngx_http_xtoken_misc_redirect(r, &conf->url_error);
	}

	expires = ngx_time() + conf->expires;
	new_expires.len = sizeof("1275396350") - 1;
	new_expires.data = ngx_pnalloc(r->pool, sizeof("1275396350"));
	if  (new_expires.data == NULL) {
		return NGX_ERROR;
	}
	ngx_snprintf(new_expires.data, new_expires.len, "%T", expires);

	if (ngx_http_xtoken_make_hash(r, conf, &new_expires, &cookie_uid, &hash) != NGX_OK) {
		return NGX_ERROR;
	}

	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_expires, &new_expires, &conf->cookie_domain, &conf->cookie_path, conf->expires + 10) != NGX_OK) {
		return NGX_ERROR;
	}
	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_hash, &hash, &conf->cookie_domain, &conf->cookie_path, conf->expires + 10)) {
		return NGX_ERROR;
	}
	if (ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_location, NULL, &conf->cookie_domain, &conf->cookie_path, 0)) {
		return NGX_ERROR;
	}

	if (ngx_http_xtoken_misc_decode_base64(r, &cookie_location, &url) == NGX_ERROR) {
		return NGX_ERROR;
	}

	/* Increment users */
	index = (ngx_time() / conf->clean_freq) % ctx->nbuckets;
	ngx_shmtx_lock(&ctx->shpool->mutex);
	shm->current++;
	shm->buckets[index]++;
	ngx_shmtx_unlock(&ctx->shpool->mutex);

	return ngx_http_xtoken_misc_redirect(r, &url);
}

static ngx_int_t ngx_http_xtoken_clear_cookies_handler(ngx_http_request_t *r)
{
	ngx_http_xtoken_loc_conf_t *conf;
	ngx_str_t tmp;

	conf = ngx_http_get_module_loc_conf(r, ngx_http_xtoken_module);

	if (!conf->enable_clear_cookies) {
		return ngx_http_next_header_filter(r);
	}

	if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_POST))) {
		return ngx_http_next_header_filter(r);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_uid, &tmp) != NGX_DECLINED) {
		ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_uid, NULL, &conf->cookie_domain, &conf->cookie_path, 0);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_hash, &tmp) != NGX_DECLINED) {
		ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_hash, NULL, &conf->cookie_domain, &conf->cookie_path, 0);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_expires, &tmp) != NGX_DECLINED) {
		ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_expires, NULL, &conf->cookie_domain, &conf->cookie_path, 0);
	}

	if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->cookie_name_location, &tmp) != NGX_DECLINED) {
		ngx_http_xtoken_misc_set_cookie(r, &conf->cookie_name_location, NULL, &conf->cookie_domain, &conf->cookie_path, 0);
	}

	return ngx_http_next_header_filter(r);
}

static ngx_int_t
ngx_http_xtoken_init_zone(ngx_shm_zone_t *shm_zone, void *data) {
	ngx_http_xtoken_ctx_t *octx;
	ngx_http_xtoken_ctx_t *ctx;

	octx = data;
	ctx = shm_zone->data;

	if (octx) {
		ctx->sh = octx->sh;
		ctx->shpool = octx->shpool;
		return NGX_OK;
	}

	ctx->shpool = (ngx_slab_pool_t *)shm_zone->shm.addr;

	if (shm_zone->shm.exists) {
		ctx->sh = ctx->shpool->data;
		return NGX_OK;
	}

	ctx->sh = ngx_slab_alloc(ctx->shpool, XTOKEN_SHM_SIZE);
	if (ctx->sh == NULL) {
		return NGX_ERROR;
	}

	ctx->shpool->data = ctx->sh;
	ngx_memzero(ctx->sh, XTOKEN_SHM_SIZE);
	return NGX_OK;
}

static char *ngx_http_xtoken_set_clear_cookies(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
	ngx_http_xtoken_loc_conf_t *c;

	c = ngx_http_conf_get_module_loc_conf(cf, ngx_http_xtoken_module);
	c->enable_clear_cookies = 1;

	return NGX_CONF_OK;
}

static char *ngx_http_xtoken_set_server(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
	ngx_http_core_loc_conf_t  *clcf;
	ngx_http_xtoken_loc_conf_t *c;
	ngx_http_xtoken_ctx_t      *ctx;
	ngx_uint_t i, nbuckets;
	ngx_int_t max_users;
	ngx_str_t tmp;

	c = ngx_http_conf_get_module_loc_conf(cf, ngx_http_xtoken_module);
	c->enable_server = 1;
	c->expires = 0;

	for (i=1; i<cf->args->nelts; i++) {
		ngx_str_t *value = cf->args->elts;

		if ((u_char *)ngx_strstr(value[i].data, "zone=") == value[i].data) {
			if (value[i].len <= sizeof("zone=") - 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"zone=\"");
				return NGX_CONF_ERROR;
			}
			c->shm_name.len = value[i].len - ngx_strlen("zone=");
			c->shm_name.data = (u_char *)(value[i].data + sizeof("zone=") - 1);
			continue;
		}

		if ((u_char *)ngx_strstr(value[i].data, "clean_freq=") == value[i].data) {
			if (value[i].len <= sizeof("clean_freq=") - 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"clean_freq=\"");
				return NGX_CONF_ERROR;
			}
			tmp.len =  value[i].len - ngx_strlen("clean_freq=");
			tmp.data = (u_char *)(value[i].data + sizeof("clean_freq=") - 1);
			c->clean_freq = ngx_parse_time(&tmp, 1);
			if (c->clean_freq == NGX_ERROR || c->clean_freq < 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value for \"clean_freq=\"");
				return NGX_CONF_ERROR;
			}
			continue;
		}

		if ((u_char *)ngx_strstr(value[i].data, "expires=") == value[i].data) {
			if (value[i].len <= sizeof("expires=") - 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"expires=\"");
				return NGX_CONF_ERROR;
			}
			tmp.len =  value[i].len - ngx_strlen("expires=");
			tmp.data = (u_char *)(value[i].data + sizeof("expires=") - 1);
			c->expires = ngx_parse_time(&tmp, 1);
			if (c->expires == NGX_ERROR || c->expires < 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value for \"expires=\"");
				return NGX_CONF_ERROR;
			}
			continue;
		}

		if ((u_char *)ngx_strstr(value[i].data, "max_users=") == value[i].data) {
			if (value[i].len <= sizeof("max_users=") - 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"max_users=\"");
				return NGX_CONF_ERROR;
			}
			tmp.len =  value[i].len - ngx_strlen("max_users=");
			tmp.data = (u_char *)(value[i].data + sizeof("max_users=") - 1);
			max_users = ngx_atoi(tmp.data, tmp.len);
			if (max_users == NGX_ERROR || max_users < 1) {
				ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value for \"max_users=\"");
				return NGX_CONF_ERROR;
			}
			c->max_users = (ngx_uint_t)max_users;
			continue;
		}

		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid arguement for xtoken_server (%V)", &value[i]);
		return NGX_CONF_ERROR;
	}

	if (c->shm_name.len == 0) {
		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "the zone name is missing");
		return NGX_CONF_ERROR;
	}

	if (c->max_users < 1) {
		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "max_users is missing");
		return NGX_CONF_ERROR;
	}

	if (c->expires < 1) {
		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "expires is wrong or missing");
		return NGX_CONF_ERROR;
	}

	nbuckets = (ngx_uint_t)(c->expires / c->clean_freq + (c->expires % c->clean_freq == 0 ? 0 : 1));

	if (nbuckets > XTOKEN_MAX_BUCKETS) {
		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please tune expires and clean_freq. expires/clean_freq=%d must be <%d", nbuckets, XTOKEN_MAX_BUCKETS);
		return NGX_CONF_ERROR;
	}

	clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
	clcf->handler = ngx_http_xtoken_server_handler;

	ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_xtoken_ctx_t));
	if (ctx == NULL) {
		return NGX_CONF_ERROR;
	}
	ctx->nbuckets = nbuckets;

	// Add more memory than necessary to handle ngx internal needs (ngx_slab_{pool,page}_t)
	c->shm_zone = ngx_shared_memory_add(cf, &c->shm_name, ngx_align(XTOKEN_SHM_SIZE + ngx_pagesize, ngx_pagesize), &ngx_http_xtoken_module);
	if (c->shm_zone == NULL) {
		return NGX_CONF_ERROR;
	}

	if (c->shm_zone->data) {
		ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "zone \"%V\" already exists", &c->shm_name);
		return NGX_CONF_ERROR;
	}

	c->shm_zone->init = ngx_http_xtoken_init_zone;
	c->shm_zone->data = ctx;

	return NGX_CONF_OK;
}

static void *ngx_http_xtoken_create_loc_conf(ngx_conf_t *cf)
{
	ngx_http_xtoken_loc_conf_t  *conf;

	conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_xtoken_loc_conf_t));
	if (conf == NULL) {
		return NULL;
	}
	conf->enable_server = NGX_CONF_UNSET;
	conf->enable_filter = NGX_CONF_UNSET;
	conf->enable_clear_cookies = NGX_CONF_UNSET;
	conf->expires_server = NGX_CONF_UNSET;
	conf->clean_freq = 10; /* 10s default */ 

	return conf;
}

static char *ngx_http_xtoken_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
	ngx_http_xtoken_loc_conf_t  *prev = parent;
	ngx_http_xtoken_loc_conf_t  *conf = child;

	ngx_conf_merge_value(conf->enable_filter, prev->enable_filter, 0);
	ngx_conf_merge_value(conf->enable_server, prev->enable_server, 0);
	ngx_conf_merge_value(conf->enable_clear_cookies, prev->enable_clear_cookies, 0);

	ngx_conf_merge_str_value(conf->cookie_name_hash, prev->cookie_name_hash, "xtoken_hash");
	ngx_conf_merge_str_value(conf->cookie_name_uid, prev->cookie_name_uid, "xtoken_uid");
	ngx_conf_merge_str_value(conf->cookie_name_location, prev->cookie_name_location, "xtoken_location");

	ngx_conf_merge_str_value(conf->cookie_domain, prev->cookie_domain, "");
	ngx_conf_merge_str_value(conf->cookie_path, prev->cookie_path, "/");

	ngx_conf_merge_str_value(conf->url_server, prev->url_server, "");
	ngx_conf_merge_str_value(conf->url_error, prev->url_error, "");
	ngx_conf_merge_str_value(conf->url_overload, prev->url_overload, "");
	ngx_conf_merge_str_value(conf->url_cookie, prev->url_cookie, "");

	ngx_conf_merge_str_value(conf->key, prev->key, "");
	ngx_conf_merge_sec_value(conf->expires_server, prev->expires_server, 10);

	if (conf->enable_filter) {
		if (conf->url_server.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_url_server must be defined when using xtoken_filter");
			return(NGX_CONF_ERROR);
		}
		if (conf->url_error.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_url_error must be defined when using xtoken_filter");
			return(NGX_CONF_ERROR);
		}
		if (conf->key.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_key must be defined when using xtoken_filter");
			return(NGX_CONF_ERROR);
		}

		if (conf->cookie_name_location.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_cookie_name must be defined when using xtoken_filter");
			return(NGX_CONF_ERROR);
		}
	}

	if (conf->enable_server) {
		if (conf->url_cookie.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_url_cookie must be defined when using xtoken_server");
			return(NGX_CONF_ERROR);
		}
		if (conf->url_error.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_url_error must be defined when using xtoken_server");
			return(NGX_CONF_ERROR);
		}
		if (conf->url_overload.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_url_overload must be defined when using xtoken_server");
			return(NGX_CONF_ERROR);
		}
		if (conf->key.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_key must be defined when using xtoken_server");
			return(NGX_CONF_ERROR);
		}
		if (conf->cookie_name_location.len == 0) {
			ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "xtoken_cookie_name must be defined when using xtoken_server");
			return(NGX_CONF_ERROR);
		}
	}

	return NGX_CONF_OK;
}

static ngx_int_t ngx_http_xtoken_init_worker(ngx_cycle_t *cycle)
{
	struct timeval  tp;

	ngx_gettimeofday(&tp);

	/* use the most significant usec part that fits to 16 bits */
	start_value = ((tp.tv_usec / 20) << 16) | ngx_pid;

	return NGX_OK;
}

static ngx_int_t ngx_http_xtoken_init(ngx_conf_t *cf)
{
	ngx_http_handler_pt        *h;
	ngx_http_core_main_conf_t  *cmcf;

	// handle xtoken_clea_cookie
	ngx_http_next_header_filter = ngx_http_top_header_filter;
	ngx_http_top_header_filter = ngx_http_xtoken_clear_cookies_handler;

	// handle xtoken_filter
	cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

	h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
	if (h == NULL) {
		return NGX_ERROR;
	}

	*h = ngx_http_xtoken_filter_handler;

	return NGX_OK;
}
